[アップデート] AWS Amplify Gen 2 が関数の非同期呼び出しをサポートしました

[アップデート] AWS Amplify Gen 2 が関数の非同期呼び出しをサポートしました

Clock Icon2024.09.18

いわさです。

今朝、Amplify で関数の非同期呼び出しをサポートしたとアナウンスがありました。

https://aws.amazon.com/about-aws/whats-new/2024/09/aws-amplify-long-running-tasks-asynchronous-function-calls/

今年の 5 月に AWS AppSync で Lambda データソースの非同期呼び出しがサポートされていたのですが、これが Amplify Gen 2 の機能としてもサポートされたという内容です。
Lambda 関数の非同期呼び出しは古くからある概念ですが、下記アップデートまでは AppSync は同期呼び出ししかサポートしてなかったのですね。

https://aws.amazon.com/about-aws/whats-new/2024/05/aws-appsync-events-asynchronous-lambda-function-invocations/

Lambda 関数の呼び出し方法には大きくは同期呼び出しと非同期呼び出しがあります。詳しくは以下の記事などを参考にしてください。
Lambda の実行結果を同期的にクライアントに返す必要がなかったり、あるいは長時間実行が想定される関数関数など、非同期呼び出しが適しているケースがあります。

https://dev.classmethod.jp/articles/lambda-idempotency/

実装:async() で AsyncFunctionHnadler を生成する

まず、非同期呼び出しの実装方法についてです。
Amplify Gen 2 では、Amplify Data の GraphQL スキーマで関数ハンドラーを構成していますが、async()メソッドが追加され AsyncFunctionHnadler が生成出来るようになっています。

https://docs.amplify.aws/react/build-a-backend/data/custom-business-logic/#async-function-handlers

まず対応が必要なのは上記なのですが、併せて Returns が設定できなくなるのでその点だけ抑えておきましょう。
あとはハンドラーの実体側も型をあわせてやる感じです。今回は次のように待機が発生する Lambda 関数を定義します。

amplify/functions/hoge0918async/handler.ts
import type { Schema } from '../../data/resource';
import { setTimeout } from "timers/promises";

export const handler: Schema["hoge0918async"]["functionHandler"] = async () => {
  await setTimeout(5000);
  return;
};

デフォルト 3 秒のタイムアウトを回避するため関数の定義でタイムアウト時間を明示的に指定しておきます。

amplify/functions/hoge0918async/handler.ts
import { defineFunction } from '@aws-amplify/backend';

export const hoge0918async = defineFunction({
  name: 'hoge0918async',
  entry: './handler.ts',
  timeoutSeconds: 100,
});

あとは先程のとおりスキーマ定義でハンドラーを設定してやりましょう。

amplify/functions/hoge0918async/handler.ts
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
import { hoge0918sync } from "../functions/hoge0918sync/resource";
import { hoge0918async } from "../functions/hoge0918async/resource";

const schema = a.schema({
  hoge0918sync: a
    .query()
    .handler(a.handler.function(hoge0918sync))
    .returns(a.string())
    .authorization((allow) => [allow.authenticated()]),
  hoge0918async: a
    .query()
    .handler(a.handler.function(hoge0918async).async())
    .authorization((allow) => [allow.authenticated()])
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "userPool",
    apiKeyAuthorizationMode: {
      expiresInDays: 30,
    },
  },
});

今回は動作の比較を行うために同期呼び出しと非同期呼び出しの関数をそれぞれ設定してみました。
関数の中身はほぼ同じで 5 秒スリープさせるものになってます。

実験:同期/非同期関数をそれぞれ呼び出してみる

実際の挙動を見てみます。
フロント側にボタンを2つ配置し、それぞれのクリックイベントでカスタムクエリを実行することで関数を呼び出します。

src/App.tsx
import { Authenticator } from '@aws-amplify/ui-react'
import '@aws-amplify/ui-react/styles.css'
import { useState } from "react";
import type { Schema } from "../amplify/data/resource";
import { generateClient } from "aws-amplify/data";

const client = generateClient<Schema>();

function App() {
  const [updateLabel, setUpdateLabel] = useState<string>()
  const executeAsyncEvent = async () => {
    const { data } = await client.queries.hoge0918async();
    console.log(data);
    setUpdateLabel("executeAsyncEvent");
  };

  const executeSyncEvent = async () => {
    const { data } = await client.queries.hoge0918sync();
    console.log(data);
    setUpdateLabel("executeSyncEvent");
  };

  return (
    <Authenticator>
      {({ signOut }) => (
        <main>
          <h1>updateLabel : {updateLabel}</h1>
          <div id="hoge-quicksight-container"></div>
          <button onClick={executeAsyncEvent}>Execute Async Event</button>
          <button onClick={executeSyncEvent}>Execute Sync Event</button>
          <button onClick={signOut}>Sign out</button>
        </main>
      )}
    </Authenticator>
  );
}

export default App;

上記では関数の呼び出し後に「updateLabel:」のラベル内容を更新するように設定しています。
このタイミングで関数呼び出し完了がどのくらいだったのかわかります。

364BA1F7-0678-47BE-9FC9-7399E9BDBE63.png

まずは同期呼び出しです。
ボタンを押してから 5 秒後にラベルが設定されました。

73087F5E-FF05-47D7-9FB9-A51C581E337C.png

フロントエンドでは関数呼び出しが完了するまで後続の処理を待機する必要がありますね。
一方で関数から直接レスポンスを受け取ることが出来るので、シンプルな実装にはなりやすい。

続いて非同期呼び出しです。
こちらはボタンを押した瞬間にラベルが設定されました。

4949EADA-891C-437B-BFBF-A769E16BCC69_4_5005_c.jpeg

ラベルは設定されても関数自体は非同期で実行され続けている形ですね。
待機せずにすぐに後続処理を実行することが出来ますが、一方で関数完了後のイベントやデータを受け取る仕組みは別途考える必要がありますね。

観察:AWS AppSync のスキーマ設定

最後に、AppSync のスキーマ設定も観察してみます。
2024 年 5 月の AppSync アップデートの記事が見当たらなかったためです。

スキーマを見てみると、同期呼び出しは String 型、非同期呼び出しは EventInvocationResponse 型が返却値となっていました。EventInvocationResponse 型は AppSync が用意したものではなく Amplify が定義した型で、関数実行の成功有無のみ Boolean 型で返すものとなってます。

1D29A2A2-B38C-47E6-91E5-09AF42485770.png

それぞれのマッピングテンプレートものぞいてみます。
以下は同期呼び出しのものです。invocationTypeRequestResponseであることが確認出来ます。

507658CC-B79C-4FC4-B3C6-A382CC6F053E.png

一方、以下は非同期呼び出しのものです。
こちらはinvocationTypeEventになっていますね。これが非同期呼び出しを指しています。

D9F57E60-92F1-4EF2-9CB3-7B021CA5710E.png

さいごに

本日は AWS Amplify Gen 2 が関数の非同期呼び出しをサポートしたので試しに使ってみました。

Amplify Gen 2 の構成から、AppSync へどのように設定されるのか、同期/非同期呼び出しでクライアントサイドの挙動がどう変わるのかを確認することが出来ました。
非同期呼び出しは結構使いたいケース多いと思いますし、どうやら昨今の生成系 AI 関係で長時間の Lambda 実行が行われることが増えているみたいですね。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.